使用Grunt实现资源自动化同步
同步美术、策划资源是日常开发中极为频繁的事情,shawn借用Web前端的一些思想和工具,将Grunt自动化框架引入Cocos Creator项目,可以实现相对高效地将图片、动画、配置、音效等游戏资源导入到客户端工程中。
grunt介绍
在开始之前先用简单介绍一下Grunt是什么:
为什么选择使用Grunt做自动化工具,简单总结以下几点:
使用JavaScript语言,与Cocos Creator开发使用相同的语言,减少学习成本
插件丰富,6000+(本篇文章只介绍两个grunt-sync和grunt-shell)
使用json配置插件完成任务,没有太多逻辑条件,使用简单容易上手,当配置好一个模块后,其它模块可以依葫芦画瓢,策划人员也可以上手配置
Grunt基于Nodejs,可以借用大量插件与npm模块实现各种复杂需求
跨平台
安装grunt与插件
首先,使用npm安装全局grunt-cli工具:
>npm install grunt-cli -g
然后在项目根目录初始化npm的包管理文件package.json:
>npm init
输入npm init后一路回车,然后在项目中安装grunt npm模块:
>npm install grunt --save-dev
grunt只是一个自动化框架,我们这里还需要安装上面说的两个插件
>npm install grunt-sync --save-dev //文件同步插件
>npm install grunt-shell --save-dev //shell插件
Grunt任务模块目录结构
安装好Grunt的命令行、插件后,在项目根目录创建Gruntfile.js文件,这是Grunt自动任务的入口文件。同时在根项目新建一个tools目录,用于存放各种与项目有关的工具或脚本,再添加一个grunt-task目录,用于存放具体的grunt任务配置脚本,请看下图:
上图中xxx-task.js就是各子模块的自动化任务。
Gruntfile
shawn在早期使用Grunt时,将所有任务都编写在Gruntfile.js文件,当模块越来越多,维护起来越来越困难,因此将不同模块的自动化任务独立开来,在Gruntfile.js进行统一加载和任务注册,下面看下Gruntfile文件的内容:
//引入rd模块读取文件
let rd = require('rd');
//获./tools/grunt-task目录下取所有文件
let taskScripts = rd.readFileSync('./tools/grunt-task');
module.exports = (grunt) => {
//我们这里使用了grunt-shel&grunt-sync插件
//下面shell与sync对象分别用于收集两种任务配置
let shell = {};
let sync = {};
//将grunt设置为全局变量
global.grunt = grunt;
//require所有任务模块,放入tasks数组
let tasks = [];
taskScripts.forEach((script) => {
let task = require(script);
if (task.init) {
//让task对象自己填充sync和shell内容
task.init(sync, shell);
tasks.push(task);
}
});
//配置sync、shell两大任务
grunt.initConfig({
sync,
shell,
});
//注册grunt-shell插件,用于执行外部shell命令
grunt.loadNpmTasks('grunt-shell');
//注册grunt-sync插件,用于本地文件同步
grunt.loadNpmTasks('grunt-sync');
//注册自定义的grunt任务
tasks.forEach(task => task.registerTask());
};
简单说明一下:
加载tools/grunt-task下所有任务脚本
为每个任务对象传入sync、shell两个任务集合对象,各任务模块在sync、shell对象中配置自己的任务内容。
使用了两个grunt插件:grunt-shell、grunt-sync
最后调用所有task.registerTask()将命令注册到grunt命令行
在命令控制台上执行grunt --help会看到我们所编写的自动化任务,下图是shawn曾经项目中创建的Grunt任务:
文件同步任务
我们现在去执行 grunt--help
还是空的,还没有注册具体的任务,前面讲过,Grunt是使用插件 + JSON配置的方式来创建任务,我们看一个美术资源为例:
资源仓库与客户端Assets按上图所示建立对应关系,其中绿色箭头是文件同步操作,其中headIcon目录中的图片,在项目中是动态加载的,需要同步到assets/resources/game1/texture目录下,这些操作我们可以使用grunt-sycn来完成。
其中比较特别是atlas目录,里面是经过分类需要合并图集的图片,文件合并后放到客户端项目assets/game1/texture/atlas目录,这个操作shawn是借用TexturePacker命令行工具 + Nodejs脚本来完成。
UI资源同步
梳理好了资源目录结构,现在我们将上流程编写成grunt同步任务,创建一个game1-task.js的文件,内容如下:
let path = require('path');
//获取grunt参数,是否模拟执行,不真实复制文件
let pretend = grunt.option('pretend');
//获取资源根路径,资源路径是定义在单独的define.js文件
let { UI_ROOT_PATH,ANI_ROOT_PATH } = require('./define');
//定义模块名,也就是在assets下的目录名
const moduleName = 'game1';
//定义同步任务
const syncTask = {
//同步UI任务:普通图片、背景图片
'sync-ui': {
files: [
//同步UI图片
{
//设置过滤器,排除atlas、headIcon、svn、隐藏文件
src: ['**', '!atlas/', '!headIcon/', '!**/.*', '!**/.svn', '!**/.svn/**'],
dest: `./assets/${moduleName}/texture/ui`, //目标路径,将文件同步到此处
cwd: path.join(UI_ROOT_PATH, moduleName, 'ui') //美术UI资源目录
},
//同步headIcon
{
//排除 atlas 目录及下面的子目录、文件
src: ['**', '!**/.*', '!**/.svn', '!**/.svn/**'],
//同步到resources/moduleName/texture目录下
dest: `./assets/resources/${moduleName}/texture`,
cwd: path.join(UI_ROOT_PATH, moduleName, 'ui', 'headIcon')
},
//同步背景图片
{
src: ['**', '!**/.*', '!**/.svn', '!**/.svn/**'],
dest: `./assets/${moduleName}/texture/bg`,
cwd: path.join(UI_ROOT_PATH, moduleName, 'bg')
}
],
verbose: true, // 显示日志
pretend: false, // 模拟输出
updateAndDelete: true, // 删除dst冗余文件
compareUsing: 'md5', // 可选'mtime/md5'
ignoreInDest: ['**/*.meta', '**/*.pac', '**/.svn/**'], // 不删除.meta文件
},
}
上面代码中sync-ui就是一个同步任务,其中files数组中配置同步目录,每一个数组元素包含三个字段:
src: 文件过滤器
dest: 目标路径,同步到那里去,以当前Gruntfile文件为相对路径
cwd: 源路径,从那里去复制文件,同样以Gruntfile文件为相对路径
然后是同步选项:
verbose: 打印日志输出,这对我们检查路径是否正确非常有用,建议设置为true
pretend: 同步模拟,当值为真时,配合verbose使用只会显示要同步的文件,不会真实写入或删除文件目标文件
updateAndDelete:删除冗余文件,比如你动随意放入一个图片到客户端ui目录,当执行ui同步资源,这个文件并未在资源仓库中,它会被同步删除掉
compareUsing:文件比较策略,可选项"md5"与“mtime”,建议使用md5保证正确性
ignoreInDest:指定同步时不删除那些文件,这个选项非常有用,我们都知道Cocos Creator会为每个文件生成同名.meta文件,这里一定要注意,不能被同步掉了,除了meta文件外,还有自动图集、svn等文件。下面是执行grunt-shell命令的效果:
⮀ grunt up-hall --pretend
Running "sync:hall-ui" (sync) task
Copying ../../../hall/ui/hall_btn_jlb.png -> assets/hall/texture/ui/hall_btn_jlb.png
Copying ../../../hall/ui/hall_btn_kefu.png -> assets/hall/texture/ui/hall_btn_kefu.png
Copying ../../../hall/ui/hall_btn_mail.png -> assets/hall/texture/ui/hall_btn_mail.png
Copying ../../../hall/ui/hall_btn_set.png -> assets/hall/texture/ui/hall_btn_set.png
Copying ../../../hall/ui/hall_btn_share.png -> assets/hall/texture/ui/hall_btn_share.png
Copying ../../../hall/ui/hall_Btn_shop.png -> assets/hall/texture/ui/hall_Btn_shop.png
Copying ../../../hall/ui/hall_btn_yxsj.png -> assets/hall/texture/ui/hall_btn_yxsj.png
Copying ../../../hall/ui/hall_img_dianchi.png -> assets/hall/texture/ui/hall_img_dianchi.png
Copying ../../../hall/ui/hall_img_dianliang.png -> assets/hall/texture/ui/hall_img_dianliang.png
Copying ../../../hall/ui/hall_img_gold.png -> assets/hall/texture/ui/hall_img_gold.png
Copying ../../../hall/ui/hall_img_head_00.png -> assets/hall/texture/ui/hall_img_head_00.png
Copying ../../../hall/ui/hall_img_head_01.png -> assets/hall/texture/ui/hall_img_head_01.png
Copying ../../../hall/ui/hall_img_laba.png -> assets/hall/texture/ui/hall_img_laba.png
Copying ../../../hall/ui/hall_img_red.png -> assets/hall/texture/ui/hall_img_red.png
Copying ../../../hall/ui/hall_img_xinhao.png -> assets/hall/texture/ui/hall_img_xinhao.png
Copying ../../../hall/ui/hall_mask_bottom.png -> assets/hall/texture/ui/hall_mask_bottom.png
Unlinking assets/resources/hall/texture/headimg/default.png because it was removed from src.
Unlinking assets/resources/hall/texture/ui/ttz_bg_toast.png because it was removed from src.
Removing dir assets/resources/hall/texture/ui because not longer in src.
上面可以看到以Copying开头的是文件复制信息,使用verbose参数,它显示了从那儿复制文件那儿,Unlinking是删除文件,同样显示了被删除的文件路径。
动画资源同步
上面讲了UI资源的同步, 对于动画资源我们处理方式有些不同,因此需要单独创建一个同步任务:
const syncTask = {
'sync-ui': { ... }
'sync-ani': {
files: [
//同步UI
{
src: ['**', '**/!.DS_Store', '!**/.svn', '!**/.svn/**', '!**/.gitignore'], //过滤器
dest: `./assets/{moduleName}/animations`,
cwd: path.join(ANI_ROOT_PATH, moduleName, 'animations')
},
],
verbose: true, // 显示日志
pretend: pretend || false, // 模拟输出
updateAndDelete: true, // 删除dst冗余文件
compareUsing: 'md5', // 可选'mtime/md5'
ignoreInDest: ['**/.svn/**', '.DS_Store', '**/.gitignore'], // 不删除.svn下文件
},
}
}
动画同步与UI同步最大的差别在于,ignoreInDes同步选项不能忽略meta文件。在shawn的项目中,动画是由美术人员在独立的Cocos Creator工程中编辑的,美术人员可以在动画工程中使用Cocos Creator动画编辑器或Spine、DragonBones等动画资源,使用Prefab进行整合,客户端主要依赖美术提供的动画prefab文件以及动画名字,动画同步任务需要将所有动画资源全部同步到客户端项目中,其中包括所有的meta文件。
这里可能会有一个小小的风险,就是动画工程中的meta文件与客户端界面中的meta文件发生UUID冲突,这种冲突的可能性是完全存在的,但在shawn一年半十多个子模块的动画项目中暂时还未遇到过,冲突的概率非常低。
图集合并同步
在UI目录中有一个类特殊的图片,需要做成图集提高游戏渲染性能,在一个游戏项目初期由于UI风格不稳定或使用临时图片,让美术同学经常去合并图集是一个效率较低的事情。因此shawn将需要合并图集的文件放入atlas的子目录中,由程序调用TexturePacker的命令行工具,以atlas子目录为单位生成图集,直接存入客户端模块atlas目录。
图集合并并完全是动态的,shawn编写了一个Node脚本,用于遍历atlas下的子目录文件,生成图集文件,然后再使用grunt-shell插件进行整合,看下面代码:
//TexturePacker图集合并工具
let tpimg = require('../tpimage');
//shell任务
const shellTask = {
//TexturePacker合并图片
'tp-img': {
command() {
//工作路径,资源仓库atlas
let cwd = path.join(UI_ROOT_PATH, `${moduleName}/ui/atlas`);
//目标路径,图集文件保存到这里
let dst = `./assets/${moduleName}/texture/ui/atlas`;
//使用编写的Node脚本生成TexturePacker命令数组
let commands = tpimg.tpdirs({ cwd, dst });
//模拟输出命令
if (pretend) {
console.log(`cwd:${cwd}, dst:${dst}`);
console.log(commands.join('\n'));
return '';
}
//返回命令字符串,由grunt-shell插件执行
return commands.join('&&');
}
},
}
下面是任务执行时的效果:
资源仓库更新
上面介绍了美术UI、动画、图集等资源的同步,但一个完整的模块资源同步,还需要涉及到对资源仓库的更新,具体操作就是用git或svn将资源仓库更新到最新状态,下面看使用grunt-shell命令更新资源仓库:
//svn更动画资源
'svn-ui': {
command: [
`svn up ${UI_ROOT_PATH}/${moduleName}/ui`,
`svn up ${UI_ROOT_PATH}/${moduleName}/bg`
].json('&&')
},
//svn更新动画资源
'svn-ani': {
//如果使用nosvn参数,返回空字符串,跳过svn更新
command: nosvn ? '' : `svn up ${ANI_ROOT_PATH}/${moduleName}/animation`,
}
有时会遇到svn暂时不可用,或有同学未安装svn命令行工具,会导致任务失败,可以shawn这里设置了一个nosvn的参数,用于控制是否跳过svn操作,直接返回了一个空字符串。
任务整合
一个子模块完整的资源同步任务大概需要经历下面几个步骤:
我们前面都建立的单个任务,使用grunt.registerTask可以将任意单个任务进行自由组合,看下图:
grunt.registerTask的参数包括:任务名、任务说明、子任务列表,子任务列表是一系列的任务或插件任务字符串的组合,上图中up-hall-svn任务,是由三个shell插件任务组成。
此时我们在项目根目录中,执行: >grunt up-hall
即可享受到所有美术图片、动画、音效、配置等等资源的同步到我们的客户端hall模块。
我们经常会遇到这样一个场景:
美术同学:“xxx程序我增加了大厅商店道具张图片,你更新一下呢,我想看看效果”。
程序同学:“这几张图需要与策划配置文件配合才能生效,yyy策划你更新下商店配置”。
策划同学:“今天早上一来我就已经更好了,你直接取吧!”。
动画同学:“商店里几个动画特效我也更新了,你也放进去一下吧”。
程序同学打开终端,键入:grunt up-hall,屏幕一阵疯狂输闪... 1、2、3、4、5 .... 五秒过后,激活Cocos Creator窗口编译资源,观察控制台,一切ok!
程序同学:“你们使用我的IP:7456自己去看吧!”
美术、策划一脸惊讶地看着你,效率这么高...
曾经十几分钟都搞不定的事情,现在几秒就解决了,每天做10几遍都不会觉得累!也不需要做10遍,将grunt任务与Cocos Creator插件结合,嵌入到Cocos Creator界面菜单上面,让程序员多休息一会儿吧!
弓箭大冒险
静待作者分享开发中的心路历程!
奎特尔数字大冒险,点击进入
0.2新版本
优化手写识别、简单难度、可爱新角色、帅气狼头
当然还有满血复活广告!
欢迎关注「奎特尔星球」公众号,欢迎大家投稿,来我们一起成长!
「奎特尔星球」微信公众号
「奎特尔星球」博客网站,建设中...